泛型接口
我们可以在接口中使用泛型,例如,Arrays.sort(Object[])
可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>
这个泛型接口:
1 | public interface Comparable<T> { |
比如说排序Person
类型:
1 | public void testPerson() { |
那么就需要在Person
类型中实现Comparable<Person>
接口:
1 | public class Person implements Comparable<Person> { |
类型擦除
Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
通过两个例子证明Java类型的类型擦除
例一:原始类型相等
1 | public class Test { |
在这个例子中,我们定义了两个ArrayList
数组,不过一个是ArrayList<String>
泛型类型的,只能存储字符串;一个是ArrayList<Integer>
泛型类型的,只能存储整数,最后,我们通过list1
对象和list2
对象的getClass()
方法获取他们的类的信息,最后发现结果为true
。说明泛型类型String
和Integer
都被擦除掉了,只剩下原始类型。
例二:通过反射添加其他类型元素
1 | public class Test { |
输出结果:
1
asd
在程序中定义了一个ArrayList
泛型类型实例化为Integer
对象,如果直接调用add()
方法,那么只能存储整数数据,不过当我们利用反射调用add()
方法的时候,却可以存储字符串,这说明了Integer
泛型实例在编译之后被擦除掉了,只保留了原始类型。
类型擦除后保留的原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
例如,我们编写了一个泛型类Pair<T>
,这是编译器看到的代码:
1 | public class Pair<T> { |
Pair的原始类型为:
1 | public class Pair { |
因为在Pair<T>
中,T 是一个无限定的类型变量,所以用Object
替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair
,如Pair<String>
或Pair<Integer>
,但是擦除类型后他们的就成为原始的Pair
类型了,原始类型都是Object
。
从上面的例2中,我们也可以明白ArrayList<Integer>
被擦除类型后,原始类型也变为Object
,所以通过反射我们就可以存储字符串了。
因此,Java使用擦拭法实现泛型,导致了:
- 编译器把类型
<T>
视为Object
; - 编译器根据
<T>
实现安全的强制转型。
使用泛型的时候,我们编写的代码也是编译器看到的代码:
1 | Pair<String> p = new Pair<>("Hello", "world"); |
而虚拟机执行的代码并没有泛型:
1 | Pair p = new Pair("Hello", "world"); |
所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T
视为Object
处理,但是,在需要转型的时候,编译器会根据T
的类型自动为我们实行安全地强制转型。
Java泛型的局限
了解了Java泛型的实现方式,我们就知道了Java泛型的局限:
局限一:<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型:
1 | Pair<int> p = new Pair<>(1, 2); // compile error! |
局限二:无法取得带泛型的Class
,就是前面的例一
1 | public static void main(String[] args) { |
局限三:无法判断带泛型的类型:
1 | Pair<Integer> p = new Pair<>(123, 456); |
原因和前面一样,并不存在Pair<String>.class
,而是只有唯一的Pair.class
。
局限四:不能实例化T
类型:
1 | public class Pair<T> { |
上述代码无法通过编译,因为构造方法的两行语句:
1 | first = new T(); |
擦拭后实际上变成了:
1 | first = new Object(); |
这样一来,创建new Pair<String>()
和创建new Pair<Integer>()
就全部成了Object
,显然编译器要阻止这种类型不对的代码。
要实例化T
类型,我们必须借助额外的Class<T>
参数:
1 | public class Pair<T> { |
上述代码借助Class<T>
参数并通过反射来实例化T
类型,使用的时候,也必须传入Class<T>
。例如:
1 | Pair<String> pair = new Pair<>(String.class); |
因为传入了Class<String>
的实例,所以我们借助String.class
就可以实例化String
类型。
泛型继承
一个类可以继承自一个泛型类。例如:父类的类型是Pair<Integer>
,子类的类型是IntPair
,可以这么继承:
1 | public class IntPair extends Pair<Integer> {} |
使用的时候,因为子类IntPair
并没有泛型类型,所以,正常使用即可:
1 | IntPair ip = new IntPair(1, 2); |
前面讲了,我们无法获取Pair<T>
的T
类型,即给定一个变量Pair<Integer> p
,无法从p
中获取到Integer
类型。
但是,在父类是泛型类型的情况下,编译器就必须把类型T
(对IntPair
来说,也就是Integer
类型)保存到子类的class文件中,不然编译器就不知道IntPair
只能存取Integer
这种类型。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair
可以获取到父类的泛型类型Integer
。
1 | public void testPair() { |
extends通配符
更详细的解释:廖雪峰的官方博客
前面定义了Pair<T>
,现在针对Pair<Number>
类型写了一个静态方法,它接收的参数类型是Pair<Number>
:
1 | public class PairHelper { |
上述代码是可以正常编译的。使用的时候,我们传入:
1 | int sum = PairHelper.add(new Pair<Number>(1, 2)); |
注意:传入的类型是Pair<Number>
,实际参数类型是(Integer, Integer)
。
既然实际参数是Integer
类型,试试传入Pair<Integer>
:
1 | public class PairHelper { |
直接运行,会得到一个编译错误:
incompatible types: Pair
cannot be converted to Pair
原因很明显,因为Pair<Integer>
不是Pair<Number>
的子类,因此,add(Pair<Number>)
不接受参数类型Pair<Integer>
。
但是从add()
方法的代码可知,传入Pair<Integer>
是完全符合内部代码的类型规范,因为语句:
1 | Number first = p.getFirst(); |
实际类型是Integer
,引用类型是Number
,没有问题。问题在于方法参数类型定死了只能传入Pair<Number>
。
有没有办法使得方法参数接受Pair<Integer>
?办法是有的,这就是使用Pair<? extends Number>
使得方法接收所有泛型类型为Number
或Number
子类的Pair
类型。
改写静态代码块:
1 | static int add(Pair<? extends Number> p) { |
这样一来,给方法传入Pair<Integer>
类型时,它符合参数Pair<? extends Number>
类型。这种使用<? extends Number>
的泛型定义称之为上界通配符,即把泛型类型T
的上界限定在Number
了。
除了可以传入Pair<Integer>
类型,我们还可以传入Pair<Double>
类型,Pair<BigDecimal>
类型等等,因为Double
和BigDecimal
都是Number
的子类。
如果我们考察对Pair<? extends Number>
类型调用getFirst()
方法,实际的方法签名变成了:
1 | <? extends Number> getFirst(); |
即返回值是Number
或Number
的子类,因此,可以安全赋值给Number
类型的变量:
1 | Number x = p.getFirst(); |
然后,我们不可预测实际类型就是Integer
,例如,下面的代码是无法通过编译的:
1 | Integer x = p.getFirst(); |
这是因为实际的返回类型可能是Integer
,也可能是Double
或者其他类型,编译器只能确定类型一定是Number
的子类(包括Number
类型本身),但具体类型无法确定。
对Pair<T>
加入set方法:
1 | public void setFirst(T first) { |
执行set方法:
1 | public static void main(String[] args) { |
发生编译错误。编译错误发生在p.setFirst()
传入的参数是Integer
类型。
如果这时候main方法里面调用Pair<Double>
,是满足Pair<? extends Number>
的,但是,Pair<Double>
的setFirst()
显然无法接受Integer
类型。
这就是<? extends Number>
通配符的一个重要限制:方法参数签名setFirst(? extends Number)
无法传递任何Number
的子类型给setFirst(? extends Number)
。
这里唯一的例外是可以给方法参数传入null
。
总的来说,使用类似<? extends Number>
通配符作为方法参数时表示:
- 方法内部可以调用获取
Number
引用的方法,例如:Number n = obj.getFirst();
; - 方法内部无法调用传入
Number
引用的方法(null
除外),例如:obj.setFirst(Number n);
。
即一句话总结:使用extends
通配符表示可以读,不能写。
super通配符
使用super
通配符来写set方法:
1 | void set(Pair<? super Integer> p, Integer first, Integer last) { |
Pair<? super Integer>
表示,方法参数接受所有泛型类型为Integer
或Integer
父类的Pair
类型。
1 | public static void main(String[] args) { |
考察Pair<? super Integer>
的setFirst()
方法,它的方法签名实际上是:
1 | void setFirst(? super Integer); |
因此,可以安全地传入Integer
类型。
再考察Pair<? super Integer>
的getFirst()
方法,它的方法签名实际上是:
1 | ? super Integer getFirst(); |
这里注意到我们无法使用Integer
类型来接收getFirst()
的返回值,即下面的语句将无法通过编译:
1 | Integer x = p.getFirst(); |
因为如果传入的实际类型是Pair<Number>
,编译器无法将Number
类型转型为Integer
。
唯一可以接收getFirst()
方法返回值的是Object
类型:
1 | Object obj = p.getFirst(); |
因此,使用<? super Integer>
通配符表示:
- 允许调用
set(? super Integer)
方法传入Integer
的引用; - 不允许调用
get()
方法获得Integer
的引用。
唯一例外是可以获取Object
的引用:Object o = p.getFirst()
。
换句话说,使用<? super Integer>
通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
参考: